Skip to main content

React

setState

  • Good read: https://chatgpt.com/share/79fab4bb-a4a9-4a58-8e55-51d216369c16

  • React's state update is asynchronous, most of the time it is fine but sometimes it creates some headache due its asynchronous + batching behavior

  • setState generally has 2 phases: State update & Re-render

  • When we call setState -> Schedule State Update in the [[Event Loop#^f7064a | Task Queue]] and mark the component as dirty

    • All these queued State Update will be batched and execute in 1 [[Event Loop#^f7064a | Event Loop]]
    • After executing the batched State Update, it will schedule the Re-render (also in the [[Event Loop#^f7064a | Task Queue]]) to be picked up by the next available iteration of the [[Event Loop#^f7064a | Event Loop]]
  • setState has 2 form: functional & non-functional

  • There's no change in scheduling mechanism between 2 forms

  • When using functional form (updater function), it guarantees to receive the most updated state (event with batching) as the state will be applied sequentially

setState(prevState => ({ count: prevState.count + 1 })); // First call
setState(prevState => ({ count: prevState.count + 1 })); // Second call
  • When both of these setState calls are batched:

    1. React doesn’t execute them immediately.
    2. It first processes the first updater function:
      • prevState.count is the current count value (say, count = 0), so it calculates count + 1 = 1 and updates the state to { count: 1 }.
    3. It then processes the second updater function:
      • This time, prevState.count is 1 (the result of the first updater), so it calculates count + 1 = 2 and updates the state to { count: 2 }.
  • At the end of the batch, React will have updated the state correctly, and only one re-render will happen with the new state { count: 2 }.

  • When you use non-functional setState calls (e.g., setState({ count: 1 })), React simply overwrites the state with the new object. But, if multiple setState calls occur in the same event, React can batch these updates as well. In this case, the last update wins, and intermediate updates may be lost if they occur before the batch is processed.

setState({ count: 1 }); // Direct update 
setState({ count: 2 }); // Overwrites previous update
  • In this case, React will batch these, and only the last state { count: 2 } is applied, resulting in one re-render with count = 2.

Deep Dive: React State Capture Patterns

The Challenge with React State Updates

When working with React state, we often need to both update the UI and capture state values at specific moments. This can be tricky due to React's asynchronous state updates.

Three Common Approaches (And Why Only One Works)

1. Direct Console Log After setState (❌ Doesn't Work)

setState(prev => [...prev, newItem])
console.log(state) // Shows old state!

This fails because:

  • React state updates are asynchronous
  • The console.log runs immediately, before React updates the state
  • You'll always see the state value from before the update

2. Console Log Inside setState (⚠️ Partially Works)

setState(prev => {
const newState = [...prev, newItem]
console.log('Inside updater:', newState) // Works, but trapped in the updater
return newState
})

This partially works because:

  • You can see the new state value inside the updater function
  • BUT the value is trapped inside the updater
  • Can't use this value outside for API calls or other operations

3. Variable Capture Attempt (❌ Doesn't Work)

let capturedState = []

setState(prev => {
capturedState = prev.slice() // This assignment happens later!
return [...prev, newItem]
})

console.log(capturedState) // Still empty! Runs before the assignment

This fails because:

  • Even though we assign to capturedState in the updater
  • The updater function runs on React's schedule
  • The console.log runs immediately, before the updater executes
  • The issue here is that React state updates are asynchronous. When you call setState, React schedules the update but doesn't apply it immediately. So if you try to access capturedState right after setState, you'll still get the old value because the state hasn't been updated yet.

The Promise Pattern Solution (✅ Works)

Basic Implementation

async function handleStateUpdate() {
const capturedState = await new Promise(resolve => {
setState(prevState => {
const currentState = prevState.slice() // Capture current state
resolve(currentState) // Make it available outside
return [...prevState, newItem] // Update UI
})
})

// Now we can use capturedState however we want!
console.log('Captured state:', capturedState)
await apiCall(capturedState)
}

Detailed Execution Flow

  1. Promise executor runs synchronously
  2. setState updater function runs synchronously
  3. resolve() captures the value we want synchronously
  4. React schedules the state update independently
  5. await ensures we wait for our captured value
// What happens in order:
const captured = await new Promise(resolve => {
// 1️⃣ This runs NOW
setState(prev => {
// 2️⃣ This also runs NOW
const valueToCapture = prev.slice()
resolve(valueToCapture) // 3️⃣ This runs NOW
return newState // 4️⃣ React schedules this update
})
})
// 5️⃣ We wait here until resolve() completes
console.log(captured) // 6️⃣ Now we have our value!

This is why the Promise approach works better - it gives us control over exactly what state we want to capture and when we want to use it, regardless of React's asynchronous state update timing.

Think of it like taking a snapshot:

  • The first approach is like trying to take a picture of something that's changing - you might miss the moment
  • The Promise approach is like having your camera ready and capturing exactly the moment you want, then being able to look at that picture later

This pattern is particularly useful when you need to:

  1. Capture a specific moment in your state's lifecycle
  2. Use that captured value after the state has changed
  3. Ensure you have the exact data you want, regardless of React's state update timing

Real-World Example: Chat Application

Here's a practical example showing why this pattern is useful:

async function handleNewMessage(newMessage) {
// 1. Capture existing messages before adding new one
const previousMessages = await new Promise(resolve => {
setMessages(prev => {
resolve(prev.slice()) // Capture current messages

// Update UI with new message + loading state
return [...prev,
{ text: newMessage, sender: 'user' },
{ text: '...typing', sender: 'bot', loading: true }
]
})
})

try {
// 2. Send only previous messages to API
const response = await api.sendMessage({
history: previousMessages, // Use captured state
newMessage: newMessage
})

// 3. Update the loading message with response
setMessages(prev => {
const current = prev.slice()
current[current.length - 1] = {
text: response.text,
sender: 'bot',
loading: false
}
return current
})
} catch (error) {
// Handle error...
}
}

Why This Pattern is Powerful

  1. Timing Control

    • Captures state exactly when we need it
    • Not affected by React's update scheduling
    • Guarantees we have the right value at the right time
  2. Independent Operations

    const captured = await new Promise(resolve => {
    setState(prev => {
    resolve(prev) // Capture happens independently
    return newState // State update happens independently
    })
    })
    • Promise resolution is separate from state update
    • React's state management remains unchanged
    • No interference with React's batching or scheduling
  3. Flexible Usage

    const captured = await new Promise(resolve => {
    setState(prev => {
    // Can capture anything we want
    resolve({
    original: prev,
    timestamp: Date.now(),
    computed: computeSomething(prev)
    })
    return newState
    })
    })

Important Distinctions

  1. This is NOT changing React's behavior

    • State updates still happen on React's schedule
    • We're just creating a way to capture values synchronously
    • React's performance optimizations remain intact
  2. Think of it like Photography

    • Regular state access: Trying to take a picture of something moving
    • Promise pattern: Capturing the exact moment we want, while letting the motion continue
  3. When to Use This Pattern

    • Need exact state at a specific moment
    • Want to use state values in async operations
    • Need to maintain UI responsiveness while processing state
    • Need to send previous state to APIs while showing optimistic UI updates

This pattern provides a reliable way to work with React's asynchronous state updates while maintaining proper timing and state management, especially useful in complex scenarios involving API calls or state-dependent operations.